#!/usr/bin/env python
#Copyright (c) 2011 Isaac Dawson (WBTS Project)
#Permission is hereby granted, free of charge, to any person obtaining a copy 
#of this software and associated documentation files (the "Software"), to deal 
#in the Software without restriction, including without limitation the rights 
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
#copies of the Software, and to permit persons to whom the Software is furnished
#to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
#THE SOFTWARE.
import uuid
import datetime
import urllib
import binascii
import json

from twisted.web import resource, server, static
from twisted.internet import defer
from zope.interface import Interface, implements
from twisted.python import components, log


# for some common filtering and response functions.
from wbts.utils import web_utils

class HeaderSetter(static.File):
    
    def getChild(self, path, request):
        encode_type = ''
        if (web_utils.not_empty(request.args, "headers")):
            headers = request.args["headers"][0]
        else:
            # No headers == nothing to do...
            return static.File.getChild(self, path, request)
            
        if (web_utils.not_empty(request.args, "encode")):
            encode_type = request.args["encode"][0]
            if (encode_type == "b64"):
                urllib.unquote(headers)
                try:
                    headers = binascii.a2b_base64(headers)
                except binascii.Error:
                    log.msg("Error base 64 decoding headers, just returning.")
                    return static.File.getChild(self, path, request)

        # we always unquote *unless* we were base64 decoded.
        if (encode_type != 'b64'):
            headers = urllib.unquote(headers)
            
        parsed_headers = web_utils.parse_headers(headers)
        for parsed_header in parsed_headers:
            if (len(parsed_header) == 2): #make sure we have 2 values
                request.setHeader(parsed_header[0], parsed_header[1])
        
        return static.File.getChild(self, path, request)


class SaveRequest(resource.Resource):
    """
    SaveRequest - Saves a request along with optional information to be
    retrieved by a unique id.  This resource is good for when you need to see
    how a request made it to a cross-origin domain from a test-case. The data
    is stored in a temporary data store called temp_store which is mapped to
    self.mMgmtService.mTempDb.
    """
    implements(resource.IResource)
    def __init__(self, service):
        """
        Saves a request using an ID to our temporary store.
        """
        self.mMgmtService = service
    
    def _cb_Err(self, err, request):
        msg = "ERROR An error occurred in saving temp data: %s"%err
        log.err(msg)
        request.write(msg)
        request.finish()
        return 
    
    def _cb_Ok(self, respdata, request):
        request.write(str(respdata))
        request.finish()
        return
    
    def render_GET(self, request):
        request.setHeader("content-type", "text/html")
        tmp_db = self.mMgmtService.mTempDb
        data = id = ''
        
        if (web_utils.not_empty(request.args, "id")):
            id = request.args["id"][0]
    
        # we need an id otherwise we won't be able to find our data.    
        if (id == ''):
            request.write('No ID specified')
            request.finish()
            return server.NOT_DONE_YET
        
        if (web_utils.not_empty(request.args, "data")):
            data = request.args["data"][0]
            
        last_request = request.channel.last_request
        request_data = "\r\n".join(last_request)
        request_data += '\r\n'+request.content.getvalue()
        
        temp_doc = {'id': id,
                    'data': binascii.b2a_base64(data),
                    'request': binascii.b2a_base64(request_data)
                    }
        
        d = tmp_db.insert(temp_doc, safe=True)
        d.addCallback(self._cb_Ok, request)
        d.addErrback(self._cb_Err, request)
        return server.NOT_DONE_YET
    
    render_POST = render_GET
    render_TRACE = render_GET
    render_OPTIONS = render_GET

class GetRequest(resource.Resource):
    """
    GetRequest - returns the request and optional data back provided the id
    exists. Can return information in xml, html or JSON format. You must pass
    an id as a parameter to retrieve a record from the temporary database.
    """
    implements(resource.IResource)
    def __init__(self, service):
        """
        Saves a request using an ID to our temporary store.
        """
        self.mMgmtService = service
    
    def _cb_Err(self, err, mode, request):
        log.err("Error occurred: %s"%err)

        if (mode == 'json'):
            request.setHeader('content-type','application/json')
            resp = {'error':str(err)}
            request.write(json.dumps(resp))
        elif (mode == 'html'):
            request.setHeader('content-type','text/html')
            resp = '<div id="error">%s</div>'%err
            request.write(resp)
        elif (mode == 'xml'):
            request.setHeader('content-type','text/xml')
            resp = '<?xml version="1.0" ?>\n'
            resp += '<result><error><![CDATA[%s]]></error></result>'%err
            request.write(resp)
        request.finish()
        return server.NOT_DONE_YET
        
            
    def _cb_responseHtml(self, result, request):
        request.setHeader('content-type','text/html')
        resp = """
<div id="id">%(id)s</div>
<div id="data">%(data)s</div>
<div id="request">%(request)s</div>
        """%({'id': result["id"],
              'data': binascii.a2b_base64(result["data"]),
              'request': binascii.a2b_base64(result["request"])})
        
        request.write(str(resp)) # i guess calling str forces serialization???
        request.finish()
        return 
    
    def _cb_responseJson(self, result, request):
        request.setHeader('content-type','application/json')
        result["_id"] = str(result["_id"])
        result["data"] = binascii.a2b_base64(result["data"])
        result["request"] = binascii.a2b_base64(result["request"])
        
        data = {}
        data["results"] = result
        request.write(json.dumps(data))
        request.finish()
        return 
    
    def _cb_responseXml(self, result, request):
        request.setHeader('content-type','text/xml')
        data = binascii.a2b_base64(result["data"])
        request_data = binascii.a2b_base64(result["request"])
        
        # make sure we don't have start/end parts of a CDATA string.
        replace_cdata(data)
        replace_cdata(request_data)
        
        resp = """
<?xml version="1.0" ?>
<result id="%(id)s">
    <data><![CDATA[%(data)s]]></data>
    <request><![CDATA[%(request)s]]></request>
</result>"""%({'id': result["id"],
           'data': data,
           'request': request_data})
        request.write(str(resp)) 
        request.finish()
        return
    
    def render_GET(self, request):
        tmp_db = self.mMgmtService.mTempDb
        mode = "html"
        cb_resp = id = None
        if (web_utils.not_empty(request.args, "id")):
            id = request.args["id"][0]
        else:
            log.err("No ID specified for GetRequest")
            return "ERROR, No ID Specified"
        
        if (web_utils.not_empty(request.args, "mode")):
            mode = request.args["mode"][0]
        
        if (mode == "html"):
            cb_resp = self._cb_responseHtml
        elif (mode == "json"):
            cb_resp = self._cb_responseJson
        elif (mode == "xml"):
            cb_resp = self._cb_responseXml
            
        if (cb_resp == None):
            request.finish()
            return "ERROR, invalid mode type"

        d = tmp_db.find_one({'id':id})
        d.addCallback(cb_resp, request)
        d.addErrback(self._cb_Err, mode, request)
        return server.NOT_DONE_YET
    
    render_POST = render_GET
        
class GetQueryOutput(resource.Resource):
    implements(resource.IResource)
    def __init__(self, resource_path):
        """
        Saves a request using an ID to our temporary store.
        """
        self._readTemplate(resource_path)
        
    def _readTemplate(self, resource_path):
        try:
            self.tmpl = open(resource_path+'query_output.template.html', 'r')
            self.resp_data = self.tmpl.read()
            self.tmpl.close()
        except IOError, msg:
            self.resp_data = "ERROR OPENING TEMPLATE"
            log.msg("An error occurred opening the template file.")
        return
    def render_GET(self, request):
        request.setHeader("content-type", "text/html")
        data = ''
        if (web_utils.not_empty(request.args, "q")):
            data = request.args["q"][0]
        
        request.write(self.resp_data%({'data': data}))
        request.finish()
        return server.NOT_DONE_YET
    
class ShowRequest(resource.Resource):
    implements(resource.IResource)
    def render_GET(self, request):
        request.setHeader("content-type", "text/html")
        data = request.channel.last_request
        request.write("\r\n".join(data))
        request.write('\r\n'+request.content.getvalue())
        request.finish()
        return server.NOT_DONE_YET
    
    render_POST = render_GET
    render_TRACE = render_GET

class GetResourceInfo(resource.Resource):
    implements(resource.IResource)
    def __init__(self, service):
        """
        Returns:
        1. The local path of resources, only really needed when you are
        testing a browser that is on the same system as WBTS is running on.
        """
        self.mMgmtService = service
        
    def render_GET(self, request):
        request.setHeader("content-type", "text/html")
        info = 'path'
        if (web_utils.not_empty(request.args, "info")):
            info = request.args["info"][0]
        if (info == 'path'):
            path = self.mMgmtService.mSharedRoot
            path = path.replace("\\","/")
            return path
        return "ERROR"
    
class SubmitTestForm(resource.Resource):
    implements(resource.IResource)
    def __init__(self, resource_path):
        """
        This somewhat falsely assumes that data/vars will be POSTed with proper
        application/x-www-url-encoded data.
        TODO: Replace the inline html junk with some sorta templating...
        """
        self._readTemplate(resource_path)
        
    def _readTemplate(self, resource_path):
        try:
            self.tmpl = open(resource_path+'test_form.template.html', 'r')
            self.resp_data = self.tmpl.read()
            self.tmpl.close()
        except IOError, msg:
            self.resp_data = "ERROR OPENING TEMPLATE"
            log.msg("An error occurred opening the template file.")
        return
        
        
    def render_POST(self, request):
        request.setHeader("content-type", "text/html")
        test_func = "tc_parsehdrs_for_crlf(xhr, tc);"
        
        client_id = description = testcase = testcase_url = input = output = ''
        expected_result = ''
        if (web_utils.not_empty(request.args, "testcase_url")):
            testcase_url = request.args["testcase_url"][0]
            client_id = web_utils.find_client_id(testcase_url)
            try:
                testcase = testcase_url[
                    testcase_url.rfind('/')+1:testcase_url.find('?')
                    ]
            except ValueError, msg:
                log.msg("Error parsing out testcase from %s from"\
                        " SubmitTestForm"%testcase_url)
        
        # determine if we are doing a header test or a file upload test
        if (web_utils.not_empty(request.args, "test_type")):
            test_type = request.args["test_type"][0]
            if (test_type == "upload"):
                test_func = "tc_parse_upload_for_data(xhr, tc);"
                
        if (web_utils.not_empty(request.args, "expected_result")):
            expected_result = request.args["expected_result"][0]
            
        if (web_utils.not_empty(request.args, "input")):
            input = request.args["input"][0]
        
        
        if (web_utils.not_empty(request.args, "description")):
            description = request.args["description"][0]
        
        # This is the actual form request data.
        data = request.channel.last_request
        output = "\r\n".join(data)
        output += "\r\n"+request.content.getvalue()

        request.write(
            self.resp_data%({'client_id': client_id,
              'testcase_url': testcase_url,
              'input': input,
              'output': output,
              'description': description,
              'expected_result': expected_result,
              'test_func': test_func
            })
        )
        request.finish()
        return server.NOT_DONE_YET
    
    render_GET = render_POST    

class RedirectRequest(resource.Resource):
    implements(resource.IResource)
    def __init__(self):
        """
        RedirectRequest resource is for allowing redirects of user
        supplied response codes and urls. We also allow the location parameter
        to be encoded (good for passing in unsafe bytes). The options are b64
        or URL.
        """
        self.mDefaultCode = 301
        self.mDefaultLoc = '/'
        self.mDefaultRedirectMsg = "The resource you requested is now at: %s"
                                   
        
    def render_GET(self, request):
        location = self.mDefaultLoc
        resp_code = self.mDefaultCode
            
        if (web_utils.not_empty(request.args, "loc")):
            location = request.args["loc"][0]
        
        if (web_utils.not_empty(request.args, "code")):
            try:
                resp_code = request.args["code"][0]
                resp_code = int(resp_code)
            except ValueError, msg:
                log.msg("ERROR RedirectRequest, response code is not an int"\
                        " defaulting to %d"%self.mDefaultCode)
                resp_code = self.mDefaultCode
                
        if (web_utils.not_empty(request.args, "encode")):
            encode_type = request.args["encode"][0]
            if (encode_type == "b64"):
                urllib.unquote(location)
                try:
                    location = binascii.a2b_base64(location)
                except binascii.Error:
                    log.err("ERROR Unable to base64 decod location, \
                            using default.")
                    location = self.mDefaultLoc
            elif (encode_type == "url"):
                location = urllib.unquote(location)
        
        request.setResponseCode(resp_code)
        request.setHeader("location", location)
        request.setHeader("content-type", "message/http")
        return (self.mDefaultRedirectMsg)%(location)
    
    render_POST = render_GET
    
        
class RebindRequest(resource.Resource):
    implements(resource.IResource)
    def __init__(self, service):
        """
        RebindRequest will take the supplied host/record type, and change that
        record to the 'new_value.' for <delay> seconds. After the record is
        found in the dns server, it will setup a timer to flip the record back
        to it's original value. This will only work if the DNS record
        already has a TTL set to 0, as the browser may cache the value and
        not even bother to look it up.
        """
        resource.Resource.__init__(self)
        self.mDnsService = service

    def _cb_Err(self, err, request):
        log.err("ERROR An error occurred while attempting to rebind!")
        log.err(err)
        return json_err_response(request, "error occurred rebinding")
    
    def _cb_Ok(self, ok, request):
        log.msg(ok)
        return json_ok_response(request)
        
    def render_GET(self, request):
        request.setHeader("content-type", "application/json")
        host = new_host = ''
        delay = 3
        params = ['authority','host','rec_type','new_value','delay']
        for param in params:
            if (check_required(request, param) == server.NOT_DONE_YET):
                return server.NOT_DONE_YET
        authority = full_escape(request.args['authority'][0])
        host = full_escape(request.args['host'][0])
        rec_type = full_escape(request.args['rec_type'][0])
        new_value = full_escape(request.args['new_value'][0])
        delay = request.args['delay'][0]
        try:
            delay = int(delay)
            if (delay < 0): # negative values are no good to us.
                delay = 3
        except ValueError:
            log.err("dns rebind test sent a non-integer value for delay.")
            return json_err_response(request, "delay must be an integer value")
        
        # check if authority exists 
        if (not self.mDnsService.doesAuthorityExist(authority)):
            return json_err_response(request, "Unable to do rebind test, authority"\
                                 " is not in our dns records!")
            
        ret = self.mDnsService.doRebind(authority, host, rec_type, new_value, delay)
        if (ret == True):
            return json_ok_response(request)
        return json_err_response(request, "Unable to do rebind test, an"\
                                 " error occurred.")
        
    render_POST = render_GET